package com.pai4j.pgc.service;
import com.pai4j.common.enums.RedisKeyEnum;
import com.pai4j.common.bean.PAIPageResponseBeanUtil;
import com.pai4j.common.constants.WebConstant;
import com.pai4j.common.config.annotation.RequireCreator;
import com.pai4j.common.enums.ArticleStatusEnum;
import com.pai4j.common.enums.DeleteFlagCodeEnum;
import com.pai4j.common.enums.messagequeue.MessageQueueEnum;
import com.pai4j.common.service.messagequeue.producer.MessageQueueProducer;
import com.pai4j.common.util.JsonUtil;
import com.pai4j.common.util.RedisUtil;
import com.pai4j.common.util.TransactionSyncManagerUtil;
import com.pai4j.domain.dto.queue.ArticleEsSyncQueueDTO;
import com.pai4j.domain.dto.queue.ArticleDelayMessageDTO;
import com.pai4j.domain.vo.request.subjectaccount.SaveArticleRequestVO;
import com.pai4j.domain.vo.response.UserBaseResponseInfoVO;
import com.pai4j.domain.vo.response.mess.SubjectAccountArticleMessageVO;
import com.pai4j.domain.vo.response.subjectaccount.ArticleBaseResponseVO;
import com.pai4j.domain.vo.response.subjectaccount.SubjectBaseResponseVO;
import com.pai4j.pgc.dao.IArticleDAO;
import com.pai4j.pgc.entity.ArticleEntity;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;
import com.pai4j.common.util.HttpClientUtil;
import com.pai4j.common.service.MinioService;
import com.pai4j.domain.vo.response.UploadResultVO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

/**
 * 专题文章服务
 *
 */
@Slf4j
@Service
public class SubjectArticleService {

    @Autowired
    private IArticleDAO articleDAO;

    @Autowired
    private AccountService accountService;
    
    @Resource
    private SubjectService subjectService;
    
    @Resource
    
    private SubjectAccountService subjectAccountService;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private MessageQueueProducer<ArticleDelayMessageDTO, Long> messageQueueProducer;
    
    @Autowired
    private MinioService minioService;
    
    @Autowired
    private MessageQueueProducer<ArticleEsSyncQueueDTO, Long> esMessageQueueProducer;
    
    @Value("${coderutil.poster.ak:}")
    private String coderutilPosterAk;
    
    @Value("${coderutil.poster.sk:}")
    private String coderutilPosterSk;

    /**
     * 保存或发布文章（统一接口）
     * 根据文章状态决定处理逻辑：
     * - status = DRAFT(0): 仅保存草稿，不加入推送队列
     * - status = WAIT_PUSH(1): 保存并加入推送队列（待推送）
     * - status = PUSHED(2): 直接发布（立即推送）
     *
     * 使用@RequireCreator注解进行创作者身份校验：
     * - allowDraft=true: 草稿状态不校验
     * - userIdParam="author": 从SaveArticleRequestVO的author字段获取用户ID
     * - statusParam="status": 从SaveArticleRequestVO的status字段判断是否草稿
     *
     * @param saveArticleRequest 文章保存请求对象
     * @return 文章ID
     */
    @Transactional
    @RequireCreator(
        message = "请先申请成为创作者后再发布文章！",
        allowDraft = true,
        userIdParam = "author",
        statusParam = "status"
    )
    public Long submit(SaveArticleRequestVO saveArticleRequest) {
        /**
         * 1. 持久化文章到数据库
         */
        ArticleEntity articleEntity = this.convert(saveArticleRequest);
        articleEntity = articleDAO.save(articleEntity);
        
        /**
         * 2. 缓存文章详情到redis
         */
        this.refreshArticleRedisDetailCache(articleEntity);
        
        // 获取文章状态
        Integer status = articleEntity.getStatus();
        final Long articleId = articleEntity.getId();
        final String subjectAccount = saveArticleRequest.getSubjectAccount();
        final Long pushTime = saveArticleRequest.getPlanPushTime();
        
        /**
         * 3. 根据文章状态决定是否加入推送队列
         * 只有非草稿状态（WAIT_PUSH 或 PUSHED）才加入推送队列
         */
        if (!ArticleStatusEnum.DRAFT.getStatus().equals(status)) {
            // 推文计划加入延迟队列（pushTime为0或null表示立即推送）
            if (pushTime != null && pushTime != 0) {
                this.doSubmitDelayQueue(subjectAccount, articleId, pushTime);
            } else {
                // 立即推送
                this.doSubmitDelayQueue(subjectAccount, articleId, System.currentTimeMillis());
            }
            
            /**
             * 4. 事务提交后，发送ES同步消息（仅非草稿状态）
             */
            TransactionSyncManagerUtil.registerSynchronization(() -> {
                try {
                    ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId, true);
                    if (article != null) {
                        ArticleEsSyncQueueDTO dto = new ArticleEsSyncQueueDTO();
                        dto.setArticleId(articleId);
                        dto.setArticleJson(JsonUtil.toJsonString(article));
                        esMessageQueueProducer.send(MessageQueueEnum.QUEUE_SUBJECT_ARTICLE_SYNC_ES, dto);
                    }
                } catch (Exception e) {
                    // 不影响主流程
                    log.error("ES同步消息发送失败: articleId={}", articleId, e);
                }
            });
        }

        return articleId;
    }


    /**
     * 专题下文章推送计划加入延迟队列
     *
     * @param subjectAccount 专题
     * @param articleId     专题文章
     * @param pushTime      设定的推送时间
     */
    private void doSubmitDelayQueue(String subjectAccount, Long articleId, Long pushTime) {
        ArticleDelayMessageDTO message = new ArticleDelayMessageDTO();
        message.setArticleId(articleId);
        message.setSubjectAccount(subjectAccount);
        // 延迟推文时间(如果未设置发布时间，则立即发布)
        pushTime = pushTime == null ? System.currentTimeMillis() : pushTime;
        message.setTime(pushTime);
        // 提交队列:优先级队列（一级队列）
        messageQueueProducer.prioritySend(MessageQueueEnum.QUEUE_SUBJECT_ARTICLE_PUSH_MESSAGE, message, pushTime);
    }


    
    /**
     * 根据文章ID刷新文章Redis详细缓存
     *
     * @param articleId 文章ID，用于查找文章实体
     * @return 刷新缓存后的文章响应对象，如果找不到文章或文章ID为空，则返回null
     */
    private ArticleBaseResponseVO refreshArticleRedisDetailCache(Long articleId) {
        if (articleId == null) {
            return null;
        }
        ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null);
        return this.refreshArticleRedisDetailCache(articleEntity);
    }
    
    /**
     * 刷新文章Redis详细缓存
     *
     * @param articleEntity 文章实体，用于生成和刷新缓存
     * @return 刷新缓存后的文章响应对象，如果文章实体为空，则返回null
     */
    private ArticleBaseResponseVO refreshArticleRedisDetailCache(ArticleEntity articleEntity) {
        if (articleEntity == null) {
            return null;
        }
        String auththor = articleEntity.getAuthor();
        UserBaseResponseInfoVO userBaseResponseInfoVO = accountService.accountInfo(auththor);
        String subjectAccount = articleEntity.getSubjectAccount();
        SubjectBaseResponseVO subjectBaseResponseVO = subjectService.getSubjectBaseInfo(Long.valueOf(subjectAccount));
        //建立文章详情缓存key
        String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleEntity.getId()));
        // 文章详情我们使用string类型来缓存，每个文章有自己的失效时间，避免缓存雪崩
        ArticleBaseResponseVO articleBaseResponseVO = this.convert(articleEntity);
        articleBaseResponseVO.setUserInfo(userBaseResponseInfoVO);
        articleBaseResponseVO.setSubjectAccountInfo(subjectBaseResponseVO);
        redisUtil.set(cacheKey, JsonUtil.toJsonString(articleBaseResponseVO), RedisKeyEnum.ARTICLE_DETAIL_CACHE.getExpireTime());
        return articleBaseResponseVO;
    }

    /**
     * 缓存空值，防止缓存击穿
     *
     * @param articleId
     */
    private void refreshArticleNoneCache(Long articleId) {
        String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId));
        // 文章详情我们使用string类型来缓存，每个文章有自己的失效时间，避免缓存雪崩
        redisUtil.set(cacheKey, WebConstant.CACHE_NONE, RedisKeyEnum.ARTICLE_DETAIL_CACHE.getExpireTime());
    }
    

    /**
     * 查询消息列表所有文章信息
     *
     * @param articleId 文章ID
     * @return 文章消息VO
     */
    public SubjectAccountArticleMessageVO getSubjectAccountArticleMessage(Long articleId) {
        // 从缓存中获取文章详情
        ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId);
        if (article == null) {
            // 如果缓存中没有，则返回null
            return null;
        }
        SubjectAccountArticleMessageVO subjectAccountArticleMessage = new SubjectAccountArticleMessageVO();
        subjectAccountArticleMessage.setTitle(article.getTitle());
        subjectAccountArticleMessage.setDescription(article.getDescription());
        // 从封面数组中取第一张图片
        subjectAccountArticleMessage.setCover(Collections.singletonList(getFirstCover(article.getCover())));
        subjectAccountArticleMessage.setArticleId(articleId);
        subjectAccountArticleMessage.setRedirectUrl(article.getRedirectUrl());
        return subjectAccountArticleMessage;
    }

    /**
     * 获取文章详情（带权限控制）
     * 草稿状态的文章只允许作者本人查看
     *
     * @param articleId 文章ID
     * @param needContent 是否需要文章内容
     * @param currentUserId 当前用户ID（用于权限验证）
     * @return 文章详情
     */
    public ArticleBaseResponseVO getArticleDetailWithPermission(Long articleId, boolean needContent, String currentUserId) {
        ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId, needContent);
        if (article == null) {
            return null;
        }
        
        // 权限检查：草稿状态的文章只能作者自己查看
        if (ArticleStatusEnum.DRAFT.getStatus().equals(article.getStatus())) {
            Assert.isTrue(StringUtils.isNotBlank(currentUserId) && currentUserId.equals(article.getAuthor()),
                    "无权限查看草稿文章");
        }
        
        return article;
    }


    /**
     * 从缓存中获取文章详情
     *
     * @param articleId 文章ID，用于标识特定的文章
     * @param needContent 是否需要文章内容，用于控制是否返回文章正文
     * @return ArticleBaseResponseVO 返回文章详情对象，如果不需内容，则内容为null
     */
    public ArticleBaseResponseVO getArticleDetailFromCache(Long articleId, boolean needContent) {
        // 从缓存中获取文章详情
        ArticleBaseResponseVO articleVO = this.getArticleDetailFromCache(articleId);
        // 如果文章详情为空，则直接返回null
        if (articleVO == null) {
            return null;
        }
        // 如果不需要文章内容，则将文章内容设置为null
        if (!needContent) {
            articleVO.setContent(null);
        }
        // 返回文章详情对象
        return articleVO;
    }

    /**
     * 查询专题文章详情
     *
     * @param articleId
     * @return
     */
    public ArticleBaseResponseVO getArticleDetailFromCache(Long articleId) {
        //cacheKey = "Web_Chat_ARTICLE_DETAIL_CACHE_atticleId"
        String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId));
        String cache = redisUtil.get(cacheKey);
        // 这里可能存在击穿问题（比如：有人恶意一直查询不存在的文章）
        // 文章缓存击穿解决办法：我们缓存一个空值
        if (StringUtils.isNotBlank(cache)) {
            if (WebConstant.CACHE_NONE.equals(cache)) {
                // 文章不存在，直接返回null，不需要在查库
                return null;
            }
            //将文章详情cache从json字符串转换为ArticleBaseResponseVO对象
            return JsonUtil.fromJson(cache, ArticleBaseResponseVO.class);
        }
        // 缓存不存在主动查询数据库，重新刷新缓存
        ArticleBaseResponseVO articleBase = this.refreshArticleRedisDetailCache(articleId);
        if (articleBase == null) {
            // 数据库文章不存在，这里缓存空值，防止redis击穿
            this.refreshArticleNoneCache(articleId);
        }
        return articleBase;
    }

    /**
     * 批量查询redis，获取文章详情缓存
     *
     * 场景：专题详情页，一次可能查询10篇文章
     * @param articleIdList 文章ID列表
     * @return 文章ID与文章详情对象的映射
     */
    public Map<Long, ArticleBaseResponseVO> batchGetArticleDetailFromCache(List<Long> articleIdList) {
        // 如果文章ID列表为空，则直接返回空映射
        if (CollectionUtils.isEmpty(articleIdList)) {
            return Collections.emptyMap();
        }
        // 初始化批量查询结果的映射
        Map<Long, ArticleBaseResponseVO> batchGetResult = new HashMap<>();
        // 构造批量查询redis的缓存key
        List<String> cacheKeys = articleIdList.stream().map(
                        id -> RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(id)))
                .collect(Collectors.toList());
        // 批量查询redis
        List<String> caches = redisUtil.mget(cacheKeys);
        // 遍历查询结果，填充批量查询结果的映射
        for (int i = 0; i < articleIdList.size(); i++) {
            Long articleId = articleIdList.get(i);
            String cache = caches.get(i);
            ArticleBaseResponseVO articleBaseResponseVO;
            // 如果缓存不为空，则解析缓存为文章详情对象
            if (StringUtils.isNotBlank(cache)) {
                articleBaseResponseVO = JsonUtil.fromJson(cache, ArticleBaseResponseVO.class);
            } else {
                // 如果缓存为空，则刷新redis中的文章详情缓存，并获取文章详情对象
                articleBaseResponseVO = this.refreshArticleRedisDetailCache(articleId);
            }
            // 将文章ID与文章详情对象映射存入结果中
            batchGetResult.put(articleId, articleBaseResponseVO);
        }
        // 返回批量查询结果的映射
        return batchGetResult;
    }

    /**
     * 分页查询文章列表（过滤已删除）
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @return 文章分页列表
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> page(Integer pageNo, Integer pageSize) {
        // 构建分页请求：通过PageRequest.of方法创建分页参数，按id字段降序排序
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "id"));
        // 查询数据库：只查询未删除的文章
        Page<ArticleEntity> articleEntities = articleDAO.findAll(pageable);
        List<ArticleBaseResponseVO> articles = new ArrayList<>();
        // 处理空数据：如果查询结果为空或内容为空，则直接返回空的文章列表。
        if (articleEntities != null && !CollectionUtils.isEmpty(articleEntities.getContent())) {
            //从分页的文章实体中批量获取专题id存入set集合中
            Set<String> subjectAccounts = articleEntities.stream().map(ArticleEntity::getSubjectAccount).collect(Collectors.toSet());
            // 走缓存批量查询专题信息
            Map<String, SubjectBaseResponseVO> accounts = subjectService.batchGet(subjectAccounts);
            // 批量构造返回文章列表参数
            /**
             *userEntities.getContent() 是从分页查询结果中获取文章实体列表的核心方法。以下是详细讲解：
             * userEntities 是通过调用 articleDAO.findAll(pageable) 方法从数据库中查询得到的分页结果对象，类型为 Page<ArticleEntity>。
             * Page<T> 是 Spring Data JPA 提供的一个接口，用于封装分页查询的结果。
             * getContent() 方法的作用
             * getContent() 是 Page<T> 接口中的一个方法，返回当前分页中的数据列表（即当前页的文章实体列表）。
             * 返回值是一个 List<ArticleEntity> 类型的集合，包含当前分页内的所有文章实体。
             **/
            articles = articleEntities.getContent().stream().map(a -> {
                ArticleBaseResponseVO article = convert(a);
                // 列表一般不需要返回详情信息，减少网络数据包传输设置为null
                article.setContent(null);
                // 这里偷懒了，建议
                article.setSubjectAccountInfo(accounts.get(a.getSubjectAccount()));
                return article;
            }).collect(Collectors.toList());
        }
        //getTotalElements() 是 Page<T> 接口中的一个方法，返回当前分页查询结果的总记录数（即满足查询条件的所有记录数，而不只是当前页的记录数）。
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, articleEntities.getTotalElements(), articles);

    }
    
    
    /**
     * 分页查询指定作者的文章（支持状态过滤，过滤已删除）
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param author 作者
     * @param status 文章状态（可选，null表示查询所有状态）
     * @return 文章分页列表
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageAuthor(Integer pageNo, Integer pageSize, String author, Integer status) {
        // 构建分页请求：通过PageRequest.of方法创建分页参数，按id字段降序排序
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "id"));
        
        // 根据是否有status参数选择查询方法（只查询未删除的文章）
        Page<ArticleEntity> articleEntities;
        Integer deleteFlag = DeleteFlagCodeEnum.UN_DELETE.getCode();
        if (status != null) {
            // 查询指定状态的未删除文章
            articleEntities = articleDAO.findByAuthorAndStatusAndDeleteFlag(author, status, deleteFlag, pageable);
        } else {
            // 查询所有状态的未删除文章
            articleEntities = articleDAO.findByAuthorAndDeleteFlag(author, deleteFlag, pageable);
        }
        
        List<ArticleBaseResponseVO> articles = new ArrayList<>();
        // 处理空数据：如果查询结果为空或内容为空，则直接返回空的文章列表
        if (articleEntities != null && !CollectionUtils.isEmpty(articleEntities.getContent())) {
            // 从分页的文章实体中批量获取专题id存入set集合中
            Set<String> subjectAccounts = articleEntities.stream()
                    .map(ArticleEntity::getSubjectAccount)
                    .collect(Collectors.toSet());
            // 走缓存批量查询专题信息
            Map<String, SubjectBaseResponseVO> accounts = subjectService.batchGet(subjectAccounts);
            
            // 批量构造返回文章列表参数
            articles = articleEntities.getContent().stream().map(a -> {
                ArticleBaseResponseVO article = convert(a);
                // 列表一般不需要返回详情信息，减少网络数据包传输设置为null
                article.setContent(null);
                article.setSubjectAccountInfo(accounts.get(a.getSubjectAccount()));
                return article;
            }).collect(Collectors.toList());
        }
        
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, articleEntities.getTotalElements(), articles);
    }

    /**
     * 分页查询指定作者的文章（不过滤状态）
     * 为了兼容旧接口，保留此方法
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param author 作者
     * @return 文章分页列表
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageAuthorAndStatusAndPublish(Integer pageNo, Integer pageSize, String author) {
        return this.pageAuthor(pageNo, pageSize, author, ArticleStatusEnum.PUSHED.getStatus());
    }
    
    /**
     * 分页查询指定作者的文章（过滤状态: 未删除的草稿）
     * 为了兼容旧接口，保留此方法
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param author 作者
     * @return 文章分页列表
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageAuthorAndStatusAndDraft(Integer pageNo, Integer pageSize, String author) {
        return this.pageAuthor(pageNo, pageSize, author, ArticleStatusEnum.DRAFT.getStatus());
    }
    
    
    
    /**
     * 将ArticleEntity对象转换为ArticleBaseResponseVO对象
     * 此方法主要用于在文章实体类和响应VO类之间进行数据转换，以便于在不同层次之间传递数据
     *
     * @param articleEntity 文章实体类对象，包含文章的详细信息
     * @return ArticleBaseResponseVO 转换后的文章响应VO对象，用于对外提供服务
     */
    private ArticleBaseResponseVO convert(ArticleEntity articleEntity) {
        // 创建文章响应VO对象实例
        ArticleBaseResponseVO articleBase = new ArticleBaseResponseVO();
        
        // 使用Spring的Bean工具类进行属性复制，减少手动赋值的繁琐
        BeanUtils.copyProperties(articleEntity, articleBase);
        
        // 确保 cover 不为 null，如果为 null 则设置为空列表
        if (articleBase.getCover() == null) {
            articleBase.setCover(new ArrayList<>());
        }
        
        // 检查计划推送日期是否为空，如果不为空则设置计划推送时间
        if (articleEntity.getPlanPushDate() != null) {
            articleBase.setPlanPushTime(articleEntity.getPlanPushDate().getTime());
        }
        
        // 检查创建日期是否为空，如果不为空则设置发布的时间
        if (articleEntity.getCreateDate() != null) {
            articleBase.setPublishTime(articleEntity.getCreateDate().getTime());
        }
        
        // 返回转换后的文章响应VO对象
        return articleBase;
    }

    /**
     * 将前端传过来的专题文章发布请求数据转换为文章实体
     * 此方法处理两种情况：更新现有文章或创建新文章
     * 对于更新操作，它从数据库中获取文章实体，检查其存在性，并验证操作权限
     * 对于创建操作，它初始化新的文章实体并设置必要的属性
     * 最后，它将请求中的数据复制到实体中，并返回实体对象
     *
     * @param saveArticleRequest 保存文章的请求对象，包含要保存或更新的文章信息
     * @return 转换后的文章实体对象
     */
    private ArticleEntity convert(SaveArticleRequestVO saveArticleRequest) {
        // 获取文章ID和作者，以及当前日期时间
        Long articleId = saveArticleRequest.getId();
        String author = saveArticleRequest.getAuthor();
        Date now = new Date();
        ArticleEntity articleEntity;
    
        // 如果文章ID不为空，则执行更新操作
        if (articleId != null) {
            // 从数据库中查找文章实体
            articleEntity = articleDAO.findById(articleId).orElse(null);
            // 断言文章存在，否则抛出异常
            Assert.notNull(articleEntity, "文章更新失败: 文章不存在！");
            // 断言文章作者匹配，以验证更新权限
            Assert.isTrue(ObjectUtils.equals(articleEntity.getAuthor(), author), "没有更新权限！");
        } else {
            // 如果文章ID为空，则执行创建新文章操作
            articleEntity = new ArticleEntity();
            // 设置创建日期、创建者、作者和初始状态
            articleEntity.setCreateDate(now);
            articleEntity.setCreateBy(author);
            articleEntity.setAuthor(author);
            // 设置删除标识为未删除
            articleEntity.setDeleteFlag(DeleteFlagCodeEnum.UN_DELETE.getCode());
            // 根据请求中的状态设置文章状态，如果未指定则默认为草稿
            Integer requestStatus = saveArticleRequest.getStatus();
            if (requestStatus != null) {
                articleEntity.setStatus(requestStatus);
            } else {
                articleEntity.setStatus(ArticleStatusEnum.DRAFT.getStatus());
            }
        }
    
        // 从请求中复制数据到文章实体
        articleEntity.setRedirectUrl(saveArticleRequest.getRedirectUrl());
        // 如果请求中指定了状态，则使用请求中的状态
        if (saveArticleRequest.getStatus() != null) {
            articleEntity.setStatus(saveArticleRequest.getStatus());
        }
        // 处理封面图片：直接设置整个列表
        if (saveArticleRequest.getCover() != null && !saveArticleRequest.getCover().isEmpty()) {
            articleEntity.setCover(saveArticleRequest.getCover());
        } else {
            articleEntity.setCover(new ArrayList<>());
        }
        articleEntity.setDescription(saveArticleRequest.getDescription());
        articleEntity.setTitle(saveArticleRequest.getTitle());
        articleEntity.setContent(saveArticleRequest.getContent());
        articleEntity.setSubjectAccount(saveArticleRequest.getSubjectAccount());
        articleEntity.setSigns(saveArticleRequest.getSigns());
    
        // 处理计划推送日期，如果请求中包含，则使用之；否则使用当前日期
        Date planPushDate = new Date();
        if (saveArticleRequest.getPlanPushTime() != null) {
            planPushDate = new Date(saveArticleRequest.getPlanPushTime());
        }
        articleEntity.setPlanPushDate(planPushDate);
    
        // 返回填充完毕的文章实体
        return articleEntity;
    }
    
    
    
    /**
     * 按照专题分页查询专题下的文章信息（过滤已删除）
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param subjectAccount 专题账户
     * @return 文章分页列表
     */
    public PAIPageResponseBeanUtil<List<ArticleBaseResponseVO>> getSubjectArticleList(Integer pageNo, Integer pageSize, Long subjectAccount) {
        
        // 创建分页请求对象，指定页码、每页大小和排序方式
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "createDate"));
        
        // 只查询未删除的文章
        Page<ArticleEntity> articleEntities = articleDAO.findAllBySubjectAccountAndDeleteFlag(
                String.valueOf(subjectAccount), 
                DeleteFlagCodeEnum.UN_DELETE.getCode(), 
                pageable);
        List<ArticleBaseResponseVO> articles = new ArrayList<>();
        // 处理空数据：如果查询结果为空或内容为空，则直接返回空的文章列表。
        if (articleEntities != null && !CollectionUtils.isEmpty(articleEntities.getContent())) {
            //从分页的专题实体中批量获取文章id存入set集合中
            Set<String> userIds = articleEntities.stream().map(ArticleEntity::getAuthor).collect(Collectors.toSet());
            // 走缓存批量查询用户信息
            Map<String, UserBaseResponseInfoVO> accounts = accountService.batchGet(userIds);
            // 批量构造返回文章列表参数
            
            articles = articleEntities.getContent().stream().map(a -> {
                ArticleBaseResponseVO article = subjectAccountService.convertBaseVo(a);
                article.setUserInfo(accounts.get(a.getAuthor()));
                return article;
            }).collect(Collectors.toList());
        }
        // 构建并返回分页响应工具类
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, articleEntities.getTotalElements(), articles);
    }
    
    /**
     * 按照专题和状态分页查询文章（过滤已删除）
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param subjectAccount 专题账户
     * @return 已推送文章分页列表
     */
    public PAIPageResponseBeanUtil<List<ArticleBaseResponseVO>> pageSubjectArticleListByStatus(Integer pageNo, Integer pageSize, Long subjectAccount) {
        
        // 创建分页请求对象，指定页码、每页大小和排序方式
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "createDate"));
        
        // 只查询已推送且未删除的文章
        Page<ArticleEntity> articleEntities = articleDAO.findAllBySubjectAccountAndStatusAndDeleteFlag(
                String.valueOf(subjectAccount), 
                ArticleStatusEnum.PUSHED.getStatus(),
                DeleteFlagCodeEnum.UN_DELETE.getCode(),
                pageable);
        List<ArticleBaseResponseVO> articles = new ArrayList<>();
        // 处理空数据：如果查询结果为空或内容为空，则直接返回空的文章列表。
        if (articleEntities != null && !CollectionUtils.isEmpty(articleEntities.getContent())) {
            //从分页的专题实体中批量获取文章id存入set集合中
            Set<String> userIds = articleEntities.stream().map(ArticleEntity::getAuthor).collect(Collectors.toSet());
            // 走缓存批量查询用户信息
            Map<String, UserBaseResponseInfoVO> accounts = accountService.batchGet(userIds);
            // 批量构造返回文章列表参数
            
            articles = articleEntities.getContent().stream().map(a -> {
                ArticleBaseResponseVO article = subjectAccountService.convertBaseVo(a);
                article.setUserInfo(accounts.get(a.getAuthor()));
                return article;
            }).collect(Collectors.toList());
        }
        // 构建并返回分页响应工具类
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, articleEntities.getTotalElements(), articles);
        
        
    }
    
    /**
     * 分页查询已发布的文章（过滤已删除）
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @return 已发布文章分页列表
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageStatus(Integer pageNo, Integer pageSize) {
        // 构建分页请求：通过PageRequest.of方法创建分页参数，按id字段降序排序
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "id"));
        // 查询已发布且未删除的文章
        Page<ArticleEntity> articleEntities = articleDAO.findByStatusAndDeleteFlag(
                ArticleStatusEnum.PUSHED.getStatus(), 
                DeleteFlagCodeEnum.UN_DELETE.getCode(), 
                pageable);
        List<ArticleBaseResponseVO> articles = new ArrayList<>();
        // 处理空数据：如果查询结果为空或内容为空，则直接返回空的文章列表。
        if (articleEntities != null && !CollectionUtils.isEmpty(articleEntities.getContent())) {
            //从分页的文章实体中批量获取专题id存入set集合中
            Set<String> subjectAccounts = articleEntities.stream().map(ArticleEntity::getSubjectAccount).collect(Collectors.toSet());
            // 走缓存批量查询专题信息
            Map<String, SubjectBaseResponseVO> accounts = subjectService.batchGet(subjectAccounts);
            // 批量构造返回文章列表参数
            articles = articleEntities.getContent().stream().map(a -> {
                ArticleBaseResponseVO article = convert(a);
                // 列表一般不需要返回详情信息，减少网络数据包传输设置为null
                article.setContent(null);
                // 这里偷懒了，建议
                article.setSubjectAccountInfo(accounts.get(a.getSubjectAccount()));
                return article;
            }).collect(Collectors.toList());
        }
        //getTotalElements() 是 Page<T> 接口中的一个方法，返回当前分页查询结果的总记录数（即满足查询条件的所有记录数，而不只是当前页的记录数）。
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, articleEntities.getTotalElements(), articles);
    }
    
    /**
     * 保存草稿文章（兼容旧接口，内部调用统一的submit方法）
     * 将文章保存为草稿状态，不进行推送处理
     *
     * @param saveArticleRequest 文章保存请求对象
     * @return 文章ID
     * @deprecated 推荐直接使用 submit() 方法并设置 status = DRAFT
     */
    public Long saveDraft(SaveArticleRequestVO saveArticleRequest) {
        // 强制设置文章状态为草稿
        saveArticleRequest.setStatus(ArticleStatusEnum.DRAFT.getStatus());
        
        //强制设置文章发布时间为0
        saveArticleRequest.setPlanPushTime(0L);
        
        // 调用统一的submit方法
        return this.submit(saveArticleRequest);
    }
    
    /**
     * 分页查询指定作者的草稿文章（兼容旧接口）
     * 内部调用统一的 pageAuthor 方法，指定状态为草稿
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param author 作者
     * @return 草稿文章分页列表
     * @deprecated 推荐直接使用 pageAuthor(pageNo, pageSize, author, ArticleStatusEnum.DRAFT.getStatus())
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageDraftsByAuthor(Integer pageNo, Integer pageSize, String author) {
        // 调用统一的分页查询方法，指定状态为草稿
        return this.pageAuthor(pageNo, pageSize, author, ArticleStatusEnum.DRAFT.getStatus());
    }
    
    /**
     * 分页查询指定专题的文章（支持状态过滤，过滤已删除）
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param subjectAccount 专题账户
     * @param status 文章状态（可选，null表示查询所有状态）
     * @return 文章分页列表
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageBySubject(Integer pageNo, Integer pageSize, String subjectAccount, Integer status) {
        // 构建分页请求：通过PageRequest.of方法创建分页参数，按更新时间降序排序
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "updateDate"));
        
        // 根据是否有status参数选择查询方法（只查询未删除的文章）
        Page<ArticleEntity> articleEntities;
        Integer deleteFlag = DeleteFlagCodeEnum.UN_DELETE.getCode();
        if (status != null) {
            // 查询指定状态的未删除文章
            articleEntities = articleDAO.findBySubjectAccountAndStatusAndDeleteFlag(subjectAccount, status, deleteFlag, pageable);
        } else {
            // 查询所有状态的未删除文章
            articleEntities = articleDAO.findAllBySubjectAccountAndDeleteFlag(subjectAccount, deleteFlag, pageable);
        }
        
        List<ArticleBaseResponseVO> articles = new ArrayList<>();
        
        // 处理查询结果
        if (articleEntities != null && !CollectionUtils.isEmpty(articleEntities.getContent())) {
            // 从分页的文章实体中批量获取作者id存入set集合中
            Set<String> userIds = articleEntities.stream()
                    .map(ArticleEntity::getAuthor)
                    .collect(Collectors.toSet());
            // 走缓存批量查询用户信息
            Map<String, UserBaseResponseInfoVO> accounts = accountService.batchGet(userIds);
            
            // 批量构造返回文章列表参数
            articles = articleEntities.getContent().stream().map(a -> {
                ArticleBaseResponseVO article = convert(a);
                // 列表不需要返回详情内容，减少网络数据包传输
                article.setContent(null);
                article.setUserInfo(accounts.get(a.getAuthor()));
                return article;
            }).collect(Collectors.toList());
        }
        
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, articleEntities.getTotalElements(), articles);
    }

    /**
     * 分页查询指定专题的草稿文章（兼容旧接口）
     * 内部调用统一的 pageBySubject 方法，指定状态为草稿
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param subjectAccount 专题账户
     * @return 草稿文章分页列表
     * @deprecated 推荐直接使用 pageBySubject(pageNo, pageSize, subjectAccount, ArticleStatusEnum.DRAFT.getStatus())
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageDraftsBySubject(Integer pageNo, Integer pageSize, String subjectAccount) {
        // 调用统一的分页查询方法，指定状态为草稿
        return this.pageBySubject(pageNo, pageSize, subjectAccount, ArticleStatusEnum.DRAFT.getStatus());
    }
    
    /**
     * 将草稿文章发布为待推送状态（兼容旧接口）
     * 直接更新文章状态和推送时间，不重新提交整个文章
     *
     * @param articleId 文章ID
     * @param author 作者（用于权限验证）
     * @param planPushTime 计划推送时间
     * @return 文章ID
     */
    public Long publishDraft(Long articleId, String author, Long planPushTime) {
        // 查找文章实体
        ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null);
        Assert.notNull(articleEntity, "文章不存在！");
        Assert.isTrue(ObjectUtils.equals(articleEntity.getAuthor(), author), "没有发布权限！");
        Assert.isTrue(ObjectUtils.equals(articleEntity.getStatus(), ArticleStatusEnum.DRAFT.getStatus()), 
                "只能发布草稿状态的文章！");
        
        // 直接更新文章状态和推送时间（不创建新记录）
        articleEntity.setStatus(ArticleStatusEnum.WAIT_PUSH.getStatus());
        
        // 更新计划推送时间
        if (planPushTime != null && planPushTime != 0) {
            articleEntity.setPlanPushDate(new Date(planPushTime));
        } else {
            articleEntity.setPlanPushDate(new Date());
        }
        
        articleEntity.setUpdateBy(author);
        articleEntity.setUpdateDate(new Date());
        
        // 保存更新（这里是UPDATE操作，不是INSERT）
        articleEntity = articleDAO.save(articleEntity);
        
        // 刷新缓存
        this.refreshArticleRedisDetailCache(articleEntity);
        
        // 加入推送队列
        Long finalPushTime = planPushTime != null && planPushTime != 0 ? 
                planPushTime : System.currentTimeMillis();
        this.doSubmitDelayQueue(articleEntity.getSubjectAccount(), articleId, finalPushTime);
        
        // 事务提交后，发送ES同步消息
        TransactionSyncManagerUtil.registerSynchronization(() -> {
            try {
                ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId, true);
                if (article != null) {
                    ArticleEsSyncQueueDTO dto = new ArticleEsSyncQueueDTO();
                    dto.setArticleId(articleId);
                    dto.setArticleJson(JsonUtil.toJsonString(article));
                    esMessageQueueProducer.send(MessageQueueEnum.QUEUE_SUBJECT_ARTICLE_SYNC_ES, dto);
                }
            } catch (Exception e) {
                log.error("ES同步消息发送失败: articleId={}", articleId, e);
            }
        });
        
        log.info("草稿发布成功: articleId={}, author={}, status: DRAFT->WAIT_PUSH", articleId, author);
        return articleId;
    }
    
    /**
     * 统计指定作者的草稿数量（过滤已删除）
     *
     * @param author 作者
     * @return 草稿数量
     */
    public Long countDraftsByAuthor(String author) {
        return articleDAO.countByAuthorAndStatusAndDeleteFlag(
                author, 
                ArticleStatusEnum.DRAFT.getStatus(),
                DeleteFlagCodeEnum.UN_DELETE.getCode());
    }
    
    /**
     * 统计指定专题的草稿数量（过滤已删除）
     *
     * @param subjectAccount 专题账户
     * @return 草稿数量
     */
    public Long countDraftsBySubject(String subjectAccount) {
        return articleDAO.countBySubjectAccountAndStatusAndDeleteFlag(
                subjectAccount, 
                ArticleStatusEnum.DRAFT.getStatus(),
                DeleteFlagCodeEnum.UN_DELETE.getCode());
    }

    /**
     * 删除文章（逻辑删除）
     * 将文章的 deleteFlag 标记为已删除，而不是物理删除数据
     * 这样可以保留文章数据用于数据分析、审计和可能的恢复
     *
     * @param articleId 文章ID
     * @param author 作者（用于权限验证）
     * @return 是否删除成功
     */
    public Boolean deleteArticle(Long articleId, String author) {
        // 查找文章实体
        ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null);
        Assert.notNull(articleEntity, "文章不存在！");
        
        // 检查是否已被删除
        if (DeleteFlagCodeEnum.DELETE.getCode().equals(articleEntity.getDeleteFlag())) {
            log.warn("文章已被删除: articleId={}", articleId);
            return true;
        }
        
        // 权限验证
        Assert.isTrue(ObjectUtils.equals(articleEntity.getAuthor(), author), "没有删除权限！");
        
        // 逻辑删除：设置删除标识和删除信息
        articleEntity.setDeleteFlag(DeleteFlagCodeEnum.DELETE.getCode());
        articleEntity.setDeleteBy(author);
        articleEntity.setDeleteDate(new Date());
        articleEntity.setUpdateBy(author);
        articleEntity.setUpdateDate(new Date());
        
        // 保存删除标记
        articleDAO.save(articleEntity);
        
        // 清除缓存（已删除的文章不应该被缓存访问）
        String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId));
        redisUtil.remove(cacheKey);
        
        log.info("文章逻辑删除成功: articleId={}, author={}, deleteFlag={}", 
                articleId, author, articleEntity.getDeleteFlag());
        return true;
    }

    /**
     * 恢复已删除的文章
     * 将逻辑删除的文章恢复为正常状态
     *
     * @param articleId 文章ID
     * @param author 作者（用于权限验证）
     * @return 是否恢复成功
     */
    public Boolean restoreArticle(Long articleId, String author) {
        // 查找文章实体
        ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null);
        Assert.notNull(articleEntity, "文章不存在！");
        
        // 检查是否已被删除
        Assert.isTrue(DeleteFlagCodeEnum.DELETE.getCode().equals(articleEntity.getDeleteFlag()),
                "文章未被删除，无需恢复！");
        
        // 权限验证
        Assert.isTrue(ObjectUtils.equals(articleEntity.getAuthor(), author), "没有恢复权限！");
        
        // 恢复：清除删除标识
        articleEntity.setDeleteFlag(DeleteFlagCodeEnum.UN_DELETE.getCode());
        articleEntity.setDeleteBy(null);
        articleEntity.setDeleteDate(null);
        articleEntity.setUpdateBy(author);
        articleEntity.setUpdateDate(new Date());
        
        // 保存恢复标记
        articleDAO.save(articleEntity);
        
        // 刷新缓存
        this.refreshArticleRedisDetailCache(articleEntity);
        
        log.info("文章恢复成功: articleId={}, author={}", articleId, author);
        return true;
    }

    /**
     * 分页查询指定作者已删除的文章
     * 用于回收站功能，方便用户查看和恢复已删除的文章
     *
     * @param pageNo 页码
     * @param pageSize 每页大小
     * @param author 作者
     * @return 已删除文章分页列表
     */
    public PAIPageResponseBeanUtil<ArticleBaseResponseVO> pageDeletedByAuthor(Integer pageNo, Integer pageSize, String author) {
        // 构建分页请求：通过PageRequest.of方法创建分页参数，按删除时间降序排序
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "deleteDate"));
        
        // 查询已删除的文章
        Page<ArticleEntity> articleEntities = articleDAO.findByAuthorAndDeleteFlag(
                author, 
                DeleteFlagCodeEnum.DELETE.getCode(), 
                pageable);
        List<ArticleBaseResponseVO> articles = new ArrayList<>();
        
        // 处理空数据
        if (articleEntities != null && !CollectionUtils.isEmpty(articleEntities.getContent())) {
            // 从分页的文章实体中批量获取专题id
            Set<String> subjectAccounts = articleEntities.stream()
                    .map(ArticleEntity::getSubjectAccount)
                    .collect(Collectors.toSet());
            // 走缓存批量查询专题信息
            Map<String, SubjectBaseResponseVO> accounts = subjectService.batchGet(subjectAccounts);
            
            // 批量构造返回文章列表参数
            articles = articleEntities.getContent().stream().map(a -> {
                ArticleBaseResponseVO article = convert(a);
                // 列表不需要返回详情内容
                article.setContent(null);
                article.setSubjectAccountInfo(accounts.get(a.getSubjectAccount()));
                return article;
            }).collect(Collectors.toList());
        }
        
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, articleEntities.getTotalElements(), articles);
    }

    /**
     * 编辑文章（统一接口，支持草稿和已发布文章的编辑）
     * 该方法可以编辑任何状态的文章，包括草稿、待推送和已发布的文章
     * - 编辑草稿：直接更新文章内容和信息，保持草稿状态
     * - 编辑已发布文章：更新文章内容，保持原状态，刷新缓存和ES索引
     * 
     * 注意：如果需要改变文章状态，请使用 updateArticleStatus() 方法
     *
     * @param saveArticleRequest 文章保存请求对象（必须包含articleId）
     * @return 文章ID
     */
    @Transactional
    public Long editArticle(SaveArticleRequestVO saveArticleRequest) {
        Assert.notNull(saveArticleRequest.getId(), "编辑文章时，文章ID不能为空！");
        
        // 查询原文章
        ArticleEntity existingArticle = articleDAO.findById(saveArticleRequest.getId()).orElse(null);
        Assert.notNull(existingArticle, "文章不存在！");
        
        // 权限验证
        Assert.isTrue(ObjectUtils.equals(existingArticle.getAuthor(), saveArticleRequest.getAuthor()),
                "没有编辑权限！");
        
        // 检查文章是否已删除
        Assert.isTrue(!DeleteFlagCodeEnum.DELETE.getCode().equals(existingArticle.getDeleteFlag()),
                "已删除的文章无法编辑！");
        
        // 保存原状态（编辑时不改变状态）
        Integer originalStatus = existingArticle.getStatus();
        
        // 如果请求中没有指定状态，则使用原状态
        if (saveArticleRequest.getStatus() == null) {
            saveArticleRequest.setStatus(originalStatus);
        }
        
        // 调用统一的submit方法进行更新
        Long articleId = this.submit(saveArticleRequest);
        
        log.info("文章编辑成功: articleId={}, author={}, status={}", 
                articleId, saveArticleRequest.getAuthor(), originalStatus);
        
        return articleId;
    }
    
    /**
     * 更新文章状态
     * 用于草稿转发布、发布转草稿等状态变更
     * 只更新状态相关字段，不创建新的文章实体
     *
     * @param articleId 文章ID
     * @param author 作者（用于权限验证）
     * @param newStatus 新状态
     * @return 是否更新成功
     */
    @Transactional
    public Boolean updateArticleStatus(Long articleId, String author, Integer newStatus) {
        // 查找文章实体
        ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null);
        Assert.notNull(articleEntity, "文章不存在！");
        Assert.isTrue(ObjectUtils.equals(articleEntity.getAuthor(), author), "没有更新权限！");
        
        // 检查文章是否已删除
        Assert.isTrue(!DeleteFlagCodeEnum.DELETE.getCode().equals(articleEntity.getDeleteFlag()),
                "已删除的文章无法更新状态！");
        
        Integer oldStatus = articleEntity.getStatus();
        
        // 如果状态没有变化，直接返回
        if (ObjectUtils.equals(oldStatus, newStatus)) {
            log.info("文章状态未变化: articleId={}, status={}", articleId, oldStatus);
            return true;
        }
        
        // 更新状态（只更新status字段，这是UPDATE操作不是INSERT）
        articleEntity.setStatus(newStatus);
        articleEntity.setUpdateBy(author);
        articleEntity.setUpdateDate(new Date());
        
        // 保存更新（JPA的save方法，如果实体有ID则是UPDATE）
        articleEntity = articleDAO.save(articleEntity);
        
        // 刷新缓存
        this.refreshArticleRedisDetailCache(articleEntity);
        
        // 如果从草稿变为发布状态，需要加入推送队列
        if (ArticleStatusEnum.DRAFT.getStatus().equals(oldStatus) 
                && !ArticleStatusEnum.DRAFT.getStatus().equals(newStatus)) {
            // 加入推送队列
            Long pushTime = System.currentTimeMillis();
            this.doSubmitDelayQueue(articleEntity.getSubjectAccount(), articleId, pushTime);
            
            // 发送ES同步消息
            TransactionSyncManagerUtil.registerSynchronization(() -> {
                try {
                    ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId, true);
                    if (article != null) {
                        ArticleEsSyncQueueDTO dto = new ArticleEsSyncQueueDTO();
                        dto.setArticleId(articleId);
                        dto.setArticleJson(JsonUtil.toJsonString(article));
                        esMessageQueueProducer.send(MessageQueueEnum.QUEUE_SUBJECT_ARTICLE_SYNC_ES, dto);
                    }
                } catch (Exception e) {
                    log.error("ES同步消息发送失败: articleId={}", articleId, e);
                }
            });
        }
        
        log.info("文章状态更新成功: articleId={}, status: {}=>{}", articleId, oldStatus, newStatus);
        return true;
    }
    
    /**
     *  根据文章详情生成分享海报
     *  UriComponentsBuilder 默认会对查询参数编码: 这里不需要加.encode(), 否则图片文字会乱码
     *
     * @author: CYM-pai
     * @date: 2025/04/30 19:58
     **/
    public String getPoster(ArticleBaseResponseVO articleBaseResponseVO) {
        Assert.notNull(articleBaseResponseVO, "articleBaseResponseVO 不能为空");
        Assert.hasText(coderutilPosterAk, "coderutil.poster.ak 必须配置");
        Assert.hasText(coderutilPosterSk, "coderutil.poster.sk 必须配置");
        final String baseUrl = "https://www.coderutil.com/api/poster/web";
        // 准备参数
        SubjectBaseResponseVO subject = articleBaseResponseVO.getSubjectAccountInfo();
        // 从封面数组中取第一张图片
        String articleCover = getFirstCover(articleBaseResponseVO.getCover());
        String img = firstNonBlank(articleCover, subject == null ? null : subject.getCover());
        String title = articleBaseResponseVO.getTitle();
        String summary = defaultIfBlank(articleBaseResponseVO.getDescription(), title);
        String name = "程序员pai";
        String qrUrl = defaultIfBlank(articleBaseResponseVO.getRedirectUrl(), "https://www.pai4j.com/");
        String urlDomain = extractDomainHost(qrUrl);
        String slogan = "助力编程提效";
        Assert.hasText(img, "海报封面图片(img)是必需的，且必须是公共URL");
        String requestUrl = UriComponentsBuilder.fromHttpUrl(baseUrl)
                .queryParam("img", img)
                .queryParam("title", title)
                .queryParam("summary", summary)
                .queryParam("name", name)
                .queryParam("url", urlDomain)
                .queryParam("slogan", slogan)
                .queryParam("qrUrl", qrUrl)
                .queryParam("_t", System.currentTimeMillis())
                .build()
                .toUriString();
        Map<String, String> headers = new HashMap<>();
        headers.put("AK", coderutilPosterAk);
        headers.put("SK", coderutilPosterSk);
        headers.put("Accept", "image/*");
        headers.put("Accept-Charset", "utf-8");
        headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
        headers.put("Referer", "https://www.pai4j.com/");
        ResponseEntity<?> responseEntity = HttpClientUtil.getObjectFromUrl(requestUrl, headers, byte[].class);
        if (responseEntity == null || responseEntity.getBody() == null) {
            throw new IllegalStateException("海报服务响应为空");
        }
        byte[] imageBytes = (byte[]) responseEntity.getBody();
        if (imageBytes == null || imageBytes.length == 0) {
            throw new IllegalStateException("海报服务返回了空的图片内容");
        }
        try {
            BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
            if (image == null) {
                throw new IllegalStateException("海报图片解码失败");
            }
            UploadResultVO uploadResult = minioService.upload(image);
            Assert.notNull(uploadResult, "上传结果不能为空");
            Assert.hasText(uploadResult.getUrl(), "上传URL不能为空");
            return uploadResult.getUrl();
        } catch (Exception e) {
            throw new RuntimeException("生成海报失败", e);
        }
    }
    
    private static String extractDomainHost(String url) {
        if (StringUtils.isBlank(url)) {
            return "";
        }
        try {
            URI uri = new URI(url);
            String host = uri.getHost();
            return host == null ? "" : host;
        } catch (URISyntaxException e) {
            return "";
        }
    }
    
    /**
     * 从封面数组中获取第一张图片
     * 
     * @param covers 封面图片列表
     * @return 第一张封面图片URL，如果列表为空则返回null
     */
    private static String getFirstCover(List<String> covers) {
        if (covers != null && !covers.isEmpty()) {
            return covers.get(0);
        }
        return null;
    }
    
    private static String firstNonBlank(String a, String b) {
        return StringUtils.isNotBlank(a) ? a : (StringUtils.isNotBlank(b) ? b : "");
    }
    
    private static String defaultIfBlank(String value, String defaultVal) {
        return StringUtils.isNotBlank(value) ? value : defaultVal;
    }
    
    /**
     *  根据文章详情生成PDF
     *
     * @author: CYM-pai
     * @date: 2025/04/30 20:01
     **/
    public static Object getPDF(ArticleBaseResponseVO articleBaseResponseVO) {
        //TODO: 待完善
        
        
        return null;
    }
    /**
     *  根据文章详情生成WORD
     *
     * @author: CYM-pai
     * @date: 2025/04/30 20:07
     **/
    public static Object getWORD(ArticleBaseResponseVO articleBaseResponseVO) {
        //TODO: 待完善
        
        
        return null;
    }
    
    /**
     * 批量逻辑删除文章
     * 根据文章ID列表批量删除文章，所有文章必须属于同一作者
     * 采用逻辑删除，文章数据会保留在数据库中
     * 删除后会同时清除Redis缓存
     *
     * @param articleIds 文章ID列表
     * @param author 作者（用于权限验证）
     * @return 成功删除的文章数量
     */
    public Integer batchDeleteArticles(List<Long> articleIds, String author) {
        if (CollectionUtils.isEmpty(articleIds)) {
            log.warn("批量删除文章失败：文章ID列表为空");
            return 0;
        }
        
        // 批量查询文章
        List<ArticleEntity> articles = articleDAO.findByIdIn(articleIds);
        if (CollectionUtils.isEmpty(articles)) {
            log.warn("批量删除文章失败：未找到任何文章");
            return 0;
        }
        
        int deletedCount = 0;
        Date now = new Date();
        
        for (ArticleEntity article : articles) {
            // 权限验证：只能删除自己的文章
            if (!ObjectUtils.equals(article.getAuthor(), author)) {
                log.warn("跳过文章删除：无权限删除文章 articleId={}, owner={}, operator={}", 
                        article.getId(), article.getAuthor(), author);
                continue;
            }
            
            // 检查是否已被删除
            if (DeleteFlagCodeEnum.DELETE.getCode().equals(article.getDeleteFlag())) {
                log.debug("跳过文章删除：文章已被删除 articleId={}", article.getId());
                continue;
            }
            
            // 逻辑删除：设置删除标识和删除信息
            article.setDeleteFlag(DeleteFlagCodeEnum.DELETE.getCode());
            article.setDeleteBy(author);
            article.setDeleteDate(now);
            article.setUpdateBy(author);
            article.setUpdateDate(now);
            
            // 保存删除标记
            articleDAO.save(article);
            
            // 清除Redis缓存
            String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(article.getId()));
            redisUtil.remove(cacheKey);
            
            deletedCount++;
            log.info("文章逻辑删除成功: articleId={}, author={}", article.getId(), author);
        }
        
        log.info("批量删除文章完成: 总数={}, 成功={}, author={}", articleIds.size(), deletedCount, author);
        return deletedCount;
    }
    
    /**
     * 批量恢复已删除的文章
     * 根据文章ID列表批量恢复文章，所有文章必须属于同一作者
     * 恢复后会重新加入缓存
     *
     * @param articleIds 文章ID列表
     * @param author 作者（用于权限验证）
     * @return 成功恢复的文章数量
     */
    public Integer batchRestoreArticles(List<Long> articleIds, String author) {
        if (CollectionUtils.isEmpty(articleIds)) {
            log.warn("批量恢复文章失败：文章ID列表为空");
            return 0;
        }
        
        // 批量查询文章
        List<ArticleEntity> articles = articleDAO.findByIdIn(articleIds);
        if (CollectionUtils.isEmpty(articles)) {
            log.warn("批量恢复文章失败：未找到任何文章");
            return 0;
        }
        
        int restoredCount = 0;
        Date now = new Date();
        
        for (ArticleEntity article : articles) {
            // 权限验证：只能恢复自己的文章
            if (!ObjectUtils.equals(article.getAuthor(), author)) {
                log.warn("跳过文章恢复：无权限恢复文章 articleId={}, owner={}, operator={}", 
                        article.getId(), article.getAuthor(), author);
                continue;
            }
            
            // 检查是否已被删除
            if (!DeleteFlagCodeEnum.DELETE.getCode().equals(article.getDeleteFlag())) {
                log.debug("跳过文章恢复：文章未被删除 articleId={}", article.getId());
                continue;
            }
            
            // 恢复：清除删除标识
            article.setDeleteFlag(DeleteFlagCodeEnum.UN_DELETE.getCode());
            article.setDeleteBy(null);
            article.setDeleteDate(null);
            article.setUpdateBy(author);
            article.setUpdateDate(now);
            
            // 保存恢复标记
            articleDAO.save(article);
            
            // 刷新Redis缓存
            this.refreshArticleRedisDetailCache(article);
            
            restoredCount++;
            log.info("文章恢复成功: articleId={}, author={}", article.getId(), author);
        }
        
        log.info("批量恢复文章完成: 总数={}, 成功={}, author={}", articleIds.size(), restoredCount, author);
        return restoredCount;
    }
    
    /**
     * 统计某专题下已发布文章的数量
     * 
     * @param subjectAccount 专题账户
     * @return 已发布文章数量
     */
    public Long countPublishedArticlesBySubject(String subjectAccount) {
        return articleDAO.countPublishedBySubjectAndDeleteFlag(
                subjectAccount, 
                ArticleStatusEnum.PUSHED.getStatus(),
                DeleteFlagCodeEnum.UN_DELETE.getCode());
    }
    
    /**
     * 统计某专题下文章的总数量（不过滤状态，只过滤已删除）
     * 
     * @param subjectAccount 专题账户
     * @return 文章总数量
     */
    public Long countTotalArticlesBySubject(String subjectAccount) {
        return articleDAO.countBySubjectAccountAndDeleteFlag(
                subjectAccount, 
                DeleteFlagCodeEnum.UN_DELETE.getCode());
    }
    
    /**
     * 物理删除文章（管理员专用，危险操作！）
     * 直接从数据库中删除文章记录，无法恢复
     * 该操作需要管理员权限，请谨慎使用
     * 删除后会清除所有相关Redis缓存
     *
     * @param articleId 文章ID
     * @param adminUser 管理员用户（用于审计记录）
     * @return 是否删除成功
     */
    public Boolean physicalDeleteArticle(Long articleId, String adminUser) {
        // 查找文章实体
        ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null);
        if (articleEntity == null) {
            log.warn("物理删除文章失败：文章不存在 articleId={}", articleId);
            return false;
        }
        
        Long id = articleEntity.getId();
        String author = articleEntity.getAuthor();
        
        // 物理删除：直接从数据库中删除
        articleDAO.delete(articleEntity);
        
        // 清除Redis缓存
        String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(id));
        redisUtil.remove(cacheKey);
        
        log.warn("【管理员操作】文章物理删除成功: articleId={}, author={}, admin={}", 
                id, author, adminUser);
        return true;
    }
    
    /**
     * 批量物理删除文章（管理员专用，危险操作！）
     * 直接从数据库中删除文章记录，无法恢复
     * 该操作需要管理员权限，请谨慎使用
     * 删除后会清除所有相关Redis缓存
     *
     * @param articleIds 文章ID列表
     * @param adminUser 管理员用户（用于审计记录）
     * @return 成功删除的文章数量
     */
    public Integer batchPhysicalDeleteArticles(List<Long> articleIds, String adminUser) {
        if (CollectionUtils.isEmpty(articleIds)) {
            log.warn("批量物理删除文章失败：文章ID列表为空");
            return 0;
        }
        
        // 批量查询文章
        List<ArticleEntity> articles = articleDAO.findByIdIn(articleIds);
        if (CollectionUtils.isEmpty(articles)) {
            log.warn("批量物理删除文章失败：未找到任何文章");
            return 0;
        }
        
        int deletedCount = 0;
        
        for (ArticleEntity article : articles) {
            Long articleId = article.getId();
            String author = article.getAuthor();
            
            // 物理删除：直接从数据库中删除
            articleDAO.delete(article);
            
            // 清除Redis缓存
            String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId));
            redisUtil.remove(cacheKey);
            
            deletedCount++;
            log.warn("【管理员操作】文章物理删除: articleId={}, author={}, admin={}", 
                    articleId, author, adminUser);
        }
        
        log.warn("【管理员操作】批量物理删除文章完成: 总数={}, 成功={}, admin={}", 
                articleIds.size(), deletedCount, adminUser);
        return deletedCount;
    }
}




